iT邦幫忙

2024 iThome 鐵人賽

DAY 10
2
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 10

Day 10: 使用 Vue Router 實現基於角色的路由權限控制

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240920/20117461VIsXy2bOk8.jpg

介紹

在開發大型應用時,確保用戶只能訪問他們有權限的頁面是非常重要的。Vue Router 提供了靈活的路由控制能力,可以結合角色權限實現應用的安全性。本文將探討如何使用 Vue Router 設計基於角色的路由權限控制,並介紹 ABAC (Attribute-Based Access Control) 和 RBAC (Role-Based Access Control) 兩種常見的訪問控制模型。

1. 什麼是 ABAC 和 RBAC?

  • RBAC (Role-Based Access Control):基於角色的訪問控制,是最常見的權限控制模型之一。用戶被賦予一個或多個角色,而每個角色對應特定的權限,進而決定用戶能訪問的資源。例如,"管理員" 角色擁有訪問所有頁面的權限,而 "普通用戶" 可能只能訪問部分頁面。

  • ABAC (Attribute-Based Access Control):基於屬性的訪問控制,除了角色之外,還根據用戶屬性(如年齡、地點、使用設備等)來決定是否允許訪問。這種模型更靈活,但實現相對複雜,適合對安全性要求較高的應用。

2. 設計角色權限和路由控制

在這篇文章中,我們將設計一個簡單的 RBAC 實現,並結合一些 ABAC 的概念,來控制用戶訪問不同的路由。

2.1 定義角色和權限

首先,我們定義一組角色和對應的路由權限。這些角色將用於控制用戶的訪問範圍。

(檔案: src/schemas/auth/roles.ts)

export enum Roles {
  ADMIN = 'admin',
  USER = 'user',
  GUEST = 'guest',
}

// 定義每個角色的訪問權限
export const rolePermissions = {
  [Roles.ADMIN]: ['dashboard', 'settings', 'profile'] as const,
  [Roles.USER]: ['dashboard', 'profile'] as const,
  [Roles.GUEST]: ['home'] as const,
} as const;

export type RolePermissionMap = typeof rolePermissions;

export type PermissionForRoles<R extends Roles> = RolePermissionMap[R][number];

export type AllPermissions = RolePermissionMap[Roles][number];

export const roleValidator = <R extends Roles>(role: R, route: AllPermissions): route is PermissionForRoles<R> => {
  const roleRouteList = rolePermissions[role];
  return roleRouteList.findIndex(item => item === route) !== -1;
};

這裡定義了三個角色:ADMINUSERGUEST,並為每個角色設置了可訪問的頁面。

2.2 設置 Vue Router 和路由守衛

接下來,我們設置 Vue Router 並實現一個簡單的路由守衛來控制訪問權限。

(檔案: src/rouer/index.ts)

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { Roles } from '../schemas/auth/roles'

export enum RouteStatus {
  Home = 'home',
  Dashboard = 'dashboard',
  Settings = 'settings',
  Profile = 'profile',
}

// 定義路由
const routes: Array<RouteRecordRaw> = [
  { 
    path: '/', 
    name: RouteStatus.Home, 
    component: () => import('../pages/Home.vue'), 
    meta: { 
      requiredRoles: [
        Roles.GUEST, 
        Roles.USER, 
        Roles.ADMIN
      ] 
    } 
  },
  { 
    path: '/dashboard', 
    name: RouteStatus.Dashboard,
    component: () => import('../pages/Dashboard.vue'), 
    meta: { 
      requiredRoles: [
        Roles.USER, 
        Roles.ADMIN
      ] 
    } 
  },
  { 
    path: '/settings', 
    name: RouteStatus.Settings, 
    component: () => import('../pages/Settings.vue'), 
    meta: { 
      requiredRoles: [
        Roles.ADMIN
      ] 
    } 
  },
  { 
    path: '/profile', 
    name: RouteStatus.Profile,
    component: () => import('../pages/Profile.vue'), 
    meta: { 
      requiredRoles: [
        Roles.USER, 
        Roles.ADMIN
      ] 
    } 
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

// 模擬當前用戶角色
const currentUserRole = Roles.USER; // 這裡可以根據實際情況設置用戶角色

const isStringArray = (input: unknown): input is string[]  => {
  if (!Array.isArray(input)) return false;
  return input.every(item => typeof item === 'string');
};

// 路由守衛:根據角色控制訪問
router.beforeEach(to => {
  const requiredRoles = to.meta.requiredRoles;
  if (!isStringArray(requiredRoles)) { 
    return { name: RouteStatus.Home }
  }

  if (requiredRoles && !requiredRoles.includes(currentUserRole)) {
    // 如果當前角色無權訪問,則到首頁
    return { name: RouteStatus.Home }
  }

  return true;
});

export default router;

這個路由守衛根據每個路由的 meta.requiredRoles 屬性來檢查當前用戶是否有權訪問該頁面。如果當前角色不在允許訪問的角色列表中,用戶將被重定向到首頁。

3. ABAC 的擴展實現

雖然 RBAC 是最基本的控制方式,我們可以進一步使用 ABAC 的思想來對訪問進行更細緻的控制,例如根據用戶屬性來決定訪問權限。這裡我們可以配合使用 zod 進行更細緻的操作。

(檔案: src/schemas/auth/abac.ts)

import { Roles } from './roles';

// 假設這是用戶信息,包含一些屬性
const currentUser = {
  role: Roles.USER,
  email: 'test@gmail.com',
  age: 25,
  location: 'Taiwan',
  isEmailVerify: true,
};

// 檢查用戶屬性是否允許訪問
import * as zod from 'zod';

// 用戶可以限制地點,年齡, 有沒有驗證 email, 某些 email, 某些角色 才可以通過,(null 則不限制)
export const checkAllowAccess = (
  user: unknown,
  constraintRoles: string[] | null, // 限制角色
  constraintEmails: string[] | null, // 限制 email,
  constraintAgeRange: [number, number] | null,  // 限制年齡 (未滿 18 歲不可以看)
  constraintLocations: string[] | null, // 限制地點 (只有台灣地區才可以看)
  constraintEmailVerify?: boolean, // 是否郵件通過驗證才可以看
): boolean => {
  const userInformationSchema = zod.object({
    roles: zod.string().refine(val => {
      if (constraintRoles === null) return true;
      return constraintRoles.includes(val);
    }),
    email: zod.string().refine(val => {
      if (constraintEmails === null) return true;
      return constraintEmails.includes(val);
    }),
    age: zod.number().refine(val => {
      if (constraintAgeRange === null) return true;
      if (constraintAgeRange[1] < constraintAgeRange[0]) return false;
      const conditionList: boolean[] = [];
      if (constraintAgeRange[0] !== -1) {
        conditionList.push(val > constraintAgeRange[0]);
      }
      if (constraintAgeRange[1] !== -1) {
        conditionList.push(val < constraintAgeRange[1]);
      }
      return conditionList.every(item => item === true);
    }),
    location: zod.string().refine(val => {
      if (constraintLocations === null) return true;
      return constraintLocations.includes(val);
    }),
    isEmailVerify: zod.boolean().refine(val => {
      if (!constraintEmailVerify) return true;
      return val;
    }),
  });
  const { success } = userInformationSchema.safeParse(user);
  return success;
};

這裡設置了一個簡單的 ABAC 驗證邏輯,當用戶訪問 settings 頁面時,將根據年齡屬性進行額外的驗證。

4. 在 Vue Router 中結合 ABAC 檢查

我們可以在路由守衛中加入 ABAC 檢查,以實現更靈活的訪問控制。

(檔案: src/router/index.ts)

import { checkAllowAccess } from '../schemas/auth/abac'

router.beforeEach(to => {
  const requiredRoles = to.meta.requiredRoles;
  if (!isStringArray(requiredRoles)) { 
    return { name: RouteStatus.Home }
  }

  if (requiredRoles && !requiredRoles.includes(currentUserRole)) {
    // 如果當前角色無權訪問,則到首頁
    return { name: RouteStatus.Home }
  }

  // 這裡更細緻的判斷 利用 ABAC 的概念
  if (!checkAllowAccess(
    currentUser,
    null, // 不限制角色
    null, // 不限制 email
    [18, -1], // 18 歲以上
    ['Taiwan'], // 只有台灣能看
  )) {
    return { name: RouteStatus.Home }
  }

  return true;
});

這樣,我們在進行角色檢查後,再使用 ABAC 進行額外的屬性驗證,進一步提升安全性和靈活性。通常 ABAC 會伴隨著一整套複雜邏輯在 pinia 管理,我們這裡簡單示範一下 ABAC 的整體應用,有可能相關資料,限制條件可能會放在 meta 處理,就像 RBAC 的處理方式一樣。

結論

通過結合 Vue Router、RBAC 和 ABAC,我們可以實現靈活且安全的路由訪問控制。RBAC 能夠快速實現基於角色的訪問控制,而 ABAC 則提供了更細緻的屬性檢查能力,使得我們可以根據應用需求設計更複雜的訪問邏輯。

希望通過這篇文章,能幫助你在 Vue 應用中設計出合理的角色權限控制機制,提升應用的安全性和用戶體驗。


上一篇
Day 9: 高階組件設計:使用 Zod 和 Vee-Validate 進行動態表單驗證
下一篇
Day 11: TypeScript 與 Pinia:如何定義強型別的 Store
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言